Agent 코드 짜다 보면
ContextVar[Optional[str]]("name", default=None)같이 대괄호와 소괄호가 연달아 나오는 줄을 자주 본다. 처음 보면 눈이 핑 돈다. 괄호 3종 세트((),[],{})가 파이썬 언어 시스템 내부에서 각각 다른 목적으로 설계되어 있기 때문. 관련: [[파이썬에서.,대괄호의 쓰임(내부에 있는 무언가를 지칭한다)]]
1. 한눈에 보는 괄호 3종 비교표
| 괄호 | 주 용도 (여러 개) | 실행 시 동작 | 메모리 |
|---|---|---|---|
() 소괄호 | 함수 호출, 객체 생성, 튜플, 연산 우선순위, 제너레이터 | 실행/생성 | 실제 객체 생성됨 |
[] 대괄호 | 리스트 리터럴, 인덱싱, 슬라이싱, 타입 파라미터, 딕셔너리 키 접근 | 꺼내기/지정 | 인덱싱은 참조만 |
{} 중괄호 | 딕셔너리 리터럴, 셋 리터럴, f-string 보간, 블록(일부 언어 아님) | 모아 담기 | dict/set 객체 생성 |
2. 소괄호 () 의 존재 이유: 실행과 객체 생성
파이썬에서 소괄호는 함수를 실행(호출) 하거나 새 객체를 생성(초기화) 할 때 사용하도록 약속되어 있다.
len("hi") # 함수 호출
ContextVar("name") # 클래스를 함수처럼 호출 → 인스턴스 생성
(1 + 2) * 3 # 연산 우선순위
(1, 2, 3) # 튜플 리터럴
(x*x for x in range(10)) # 제너레이터 표현식만약 타입 힌트를 작성할 때 ContextVar(Optional[str])처럼 소괄호를 쓰면, 파이썬은 “타입 규격 정의”로 안 본다. “지금 당장 Optional[str]이라는 값을 넣어 새 ContextVar 객체를 생성하라”고 해석하여 TypeError를 발생시킨다.
주의: 요소 하나짜리 튜플
a = (1) # 그냥 정수 1, 소괄호는 우선순위로 해석됨
b = (1,) # 튜플 (1,) — 쉼표가 핵심
c = 1, 2, 3 # 소괄호 없어도 튜플3. 대괄호 [] 의 존재 이유 : 내부 타입 파라미터(규격) 지정 + 인덱싱
대괄호는 본래 리스트 리터럴이나 인덱싱/슬라이싱에 쓰이지만, 타입 힌트 문법에서는 객체를 실제로 생성하지 않고 “이 컨테이너가 담을 데이터의 논리적 종류” 만 선언하도록 특별하게 설계되었다.
내부적으로 __class_getitem__(클래스 제네릭 파라미터 전용 특수 메서드)이 호출되어 메모리 낭비 없이 타입 정보만 안전하게 정의한다.
대괄호가 쓰이는 모든 맥락
| 용도 | 예시 | 내부 동작 |
|---|---|---|
| 리스트 리터럴 | [1, 2, 3] | 리스트 객체 생성 |
| 인덱싱 | lst[0], d["key"] | __getitem__ 호출 |
| 슬라이싱 | s[1:5:2] | __getitem__(slice 객체) |
| 제네릭 타입 파라미터 | List[int], Dict[str, Any] | __class_getitem__ 호출 |
딕셔너리 unpacking (X, 이건 ** 씀) | - | - |
4. 실제 예시 : 둘이 한 줄에 같이 나올 때
import contextvars
from typing import Optional
_active_session_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
"langfuse_session_id", default=None
)단계 분해
1단계 → contextvars.ContextVar[Optional[str]] : 대괄호로 타입 규격 선언. “이 변수는 Optional[str]을 담는 ContextVar다”라는 설계도. 실제 객체는 아직 없음
2단계 → contextvars.ContextVar("langfuse_session_id", default=None) : 소괄호로 진짜 객체 생성. “이름은 langfuse_session_id, 기본값은 None인 ContextVar를 지금 만들어라”라는 실행 명령
3단계 → 결과적으로 _active_session_id 변수에 생성된 객체가 할당됨
비유
건물 짓기와 같다. 대괄호는 도면(설계도) 을 그리는 행위, 소괄호는 진짜 철근 박고 건물 올리는 행위. 도면만 있다고 건물이 생기는 건 아니고, 도면대로 지어야 실제 사용 가능한 건물(객체)이 나온다.
5. 중괄호 {} 도 잊지 말기
파이썬에는 괄호가 사실 3종류다.
{"key": "value"} # 딕셔너리 리터럴
{1, 2, 3} # 셋(set) 리터럴
{} # 빈 딕셔너리 (빈 셋은 set())
f"hello {name}" # f-string 내부 보간Agent 코드에서 자주 보이는 것들:
# Pydantic 모델 인스턴스 → dict 변환
user.model_dump() # {"name": "John", "age": 30}
# LangGraph 상태 스키마
class State(TypedDict):
messages: list
context: dict6. 헷갈리기 쉬운 조합 패턴
6-1. 데코레이터 인자 있음 vs 없음
@app.get("/users") # 소괄호 있음 → 함수 호출 결과가 데코레이터
def list_users(): ...
@staticmethod # 소괄호 없음 → 함수 자체가 데코레이터
def helper(): ...6-2. 제네릭 타입 힌트에서 대괄호 중첩
List[Dict[str, int]] # 딕셔너리를 담는 리스트
Callable[[int, str], bool] # (int, str) → bool 함수 타입
Optional[List[int]] # List[int] 또는 None외부 대괄호는 바깥 컨테이너 타입 파라미터, 내부 대괄호는 중첩된 타입 파라미터. 타입 힌트 안에선 메모리에 뭔가 만들어지지 않는다는 점만 기억하면 됨.
6-3. 람다 vs 함수
f = lambda x: x * 2 # 람다 정의, 소괄호 없음
f(5) # 소괄호로 호출 → 107. 직접 확인
>>> from typing import List
>>> type(List[int])
<class 'typing._GenericAlias'> # 타입 객체, 값 저장 X
>>> type(list([1,2,3]))
<class 'list'> # 실제 리스트 객체
>>> (1) # 정수
1
>>> (1,) # 튜플
(1,)
>>> [1] # 리스트
[1]
>>> {1} # 셋
{1}
>>> {"a": 1} # 딕셔너리
{'a': 1}8. 왜 이렇게 설계됐는가
- 명확한 역할 분리: 컴파일러/인터프리터가 문맥 보지 않고도 토큰만으로 의도를 구분할 수 있어야 함
- 제네릭 타입 힌트는 나중에 추가된 문법이라 기존 인덱싱 문법(
[]) 위에 얹힘.__class_getitem__이라는 별도 훅(hook)을 추가해서 기존 코드와 충돌 안 나게 설계 - 소괄호 = 동사, 대괄호 = 목적어라는 감각으로 잡으면 대부분 해결됨
9. 한마디 요약
대괄호 [] 는 “이 변수는 이런 형태 담을 것이다”는 설계도(타입 힌트, 인덱스), 소괄호 () 는 “이제 설계도대로 실제 객체 만들어라”는 실행 명령, 중괄호 {} 는 “여러 개를 키-값 또는 셋으로 묶어라”는 모음통.